package server.app.fileupload;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import annotation.Inject;
import annotation.Service;
import common.ConfigHelper;
import common.NotifyWaiter;

@Service(name = "upload", applaction = "/upload")
public class NginxFileUploadHandler extends AbstractHandler implements ServletFileUploadProvider
{
    private long sizeMax = -1;
    ObjectMapper mapper = new ObjectMapper();
    private ExecutorService executorService;
    ScheduledExecutorService scheduledService = Executors.newSingleThreadScheduledExecutor();
    final static int maxOperateTime = 3;
    private Queue<FileData> queue = new LinkedList<FileData>();

    DiskFileItemFactory factory;
    AtomicLong index = new AtomicLong(0);
    File filesRoot;
    static FileUploadConfig config = new FileUploadConfig();

    public NginxFileUploadHandler()
    {
        this.executorService = Executors.newFixedThreadPool(config.fileUploadPoolSize);
        scheduledService.scheduleAtFixedRate(new CleanUp(queue), config.initialDelay, config.period, TimeUnit.SECONDS);
        factory = new DiskFileItemFactory();
    }

    @Inject(name = "FileUploadHandler.file", param = "app.fileupload.root", type = File.class)
    public void initRepository(File filesRoot)
    {
        this.filesRoot = filesRoot;
        if (null != factory && null != filesRoot)
        {
            factory.setRepository(filesRoot);
        }

    }

    public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
    {
        baseRequest.setHandled(true);
        response.setContentType("text/html;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_OK);
        boolean isMultipart = ServletFileUpload.isMultipartContent(request);
        if (!isMultipart)
        {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        Future<List<FileData>> future = executorService.submit(new FileUploadRequest(this, request, response));
        List<FileData> list = null;
        try
        {
            list = waitForResults(future, config.uploadTimeout);
        }
        catch (Exception e1)
        {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
        if (null == list)
        {
            response.setStatus(HttpServletResponse.SC_REQUEST_TIMEOUT);
            return;
        }
        try
        {
            convertFileData(response, list);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        synchronized (queue)
        {
            for (FileData file : list)
            {
                queue.add(file);
            }
        }
        response.setStatus(HttpServletResponse.SC_OK);
    }

    private void convertFileData(HttpServletResponse response, List<FileData> fileDataList)
                    throws JsonGenerationException, JsonMappingException, IOException
    {

        for (FileData fileData : fileDataList)
        {
            // set work flow step 1 for preparatory phase.
            fileData.setStep(1);
            // set referer host
            // String baseUrl = ConfigHelper.getStringProperty("app.fileupload.server");

            String subFolder = config.getResPath(fileData.getContentType());

            String name = fileData.getName();
            String fileSuffix = "";
            if (StringUtils.contains(name, "."))
            {
                fileSuffix = "." + getFilePostfix(name);
            }
            String uploadtemppath = StringUtils.substringAfterLast(fileData.getPath(), "/");

            fileData.addValue(FileUploadEnum.subfolder, subFolder);
            fileData.addValue(FileUploadEnum.file, fileData.getName());
            fileData.addValue(FileUploadEnum.reFile, uploadtemppath + fileSuffix);
            putValue(fileData, subFolder, FileUploadEnum.keepFileName);

            putAttr(fileData, subFolder, FileUploadEnum.basefolder);
            String[] keys = ConfigHelper.getPossibleUpwardKeys(config.PREFIX, subFolder,
                FileUploadEnum.json.toString() + "_*");
            if (null != keys)
            {
                for (String key : keys)
                {
                    String attrName =
                                    FileUploadEnum.json.toString() + "_"
                                                    + StringUtils.substringAfterLast(key, "_");
                    fileData.addAttr(attrName, ConfigHelper.getStringProperty(key));
                }
            }
            // preparatory phase finish.

            // set work flow step 2 for parsing parameter phase
            fileData.setStep(2);
            fileData.parsed();
        }

        // Prepare the content.
        List<Map<String, String>> datas = new ArrayList<Map<String, String>>();
        for (FileData data : fileDataList)
        {
            datas.add(data.getResponseObjWithJSONPrefix());
        }

        // Prepare to response content
        String characterEncoding = "utf-8";
        response.setContentType("application/json");
        response.setCharacterEncoding(characterEncoding);
        String content = mapper.writeValueAsString(datas);
        int length = content.length();
        ByteArrayOutputStream arrayStream = new ByteArrayOutputStream((int) (length * 1.2));
        PrintStream stream = new PrintStream(arrayStream, false, characterEncoding);
        stream.print(content);
        stream.close();
        arrayStream.close();
        response.setContentLength(arrayStream.size());
        arrayStream.writeTo(response.getOutputStream());
        response.getOutputStream().close();
    }

    synchronized public <T> boolean waitForCompletion(Future<T> Future, long timeout)
    {
        NotifyWaiter waiter = new NotifyWaiter(this, timeout);
        while (!Future.isDone())
        {
            if (waiter.waitForNotify() == false)
            {
                break;
            }
        }
        return Future.isDone();
    }

    public <T> T waitForResults(Future<T> Future, long timeout) throws Exception
    {
        while (true)
        {
            if (Future.isDone())
            {
                break;
            }
            Thread.sleep(config.initialDelay * 1000);
        }
        return Future.get();
    }

    public ServletFileUpload getFileUpload()
    {
        // TODO should be optimed
        return new ServletFileUpload(factory);
    }

    static class CleanUp implements Runnable
    {
        private Queue<FileData> cleanUpQueue = null;
        boolean isWaitAllTaskFinished = false;

        public CleanUp(Queue<FileData> queue)
        {
            this(queue, false);
        }

        public CleanUp(Queue<FileData> queue, boolean isWaitAllTaskFinished)
        {
            this.cleanUpQueue = queue;
            this.isWaitAllTaskFinished = isWaitAllTaskFinished;
        }

        public void run()
        {
            FileData fileData = null;
            ArrayList<FileData> errorList = new ArrayList<FileData>();
            int leftCount = config.maxCountHandleImageMoveProcess;
            synchronized (cleanUpQueue)
            {
                fileData = cleanUpQueue.poll();
            }
            while ((leftCount--) > 0 && fileData != null)
            {
                // move file

                String descFilePath = getDescFilePath(fileData);
                // String fileName = StringUtils.substringAfterLast(descFilePath, "/");
                // String path = StringUtils.replace(descFilePath, fileName, "");
                File descDir = FileUtils.getFile(descFilePath);

                if (!descDir.exists())
                {
                    descDir.mkdirs();
                }
                // the refile name is nginx made template file's name. and plus wit real filen
                // postfix.
                String fileName = fileData.getValues().get(FileUploadEnum.reFile);
                try
                {
                    // move the template file to destination directory.

                    FileUtils.moveFileToDirectory(FileUtils.getFile(fileData.getPath()), descDir, true);
                    // the file without suffix.
                    File file = FileUtils.getFile(descDir, StringUtils.substringBeforeLast(fileName, "."));
                    if (!file.exists())
                    {
                        System.out.println(String.format("file exists :(%s)", file.getPath()));

                        throw new RuntimeException(
                                        String.format(
                                            "MOVE FILE FAIL : the file content type is %s.\n source file path is \" %s \" is exists,but the target file \" %s%s \" isn't exists.  ",
                                            fileData.getContentType(), file.getPath(), descDir, fileName));

                    }
                    String flag = fileData.getValues().get(FileUploadEnum.keepFileName);
                    // if we want still use the file original name,assigned the Keepfilename
                    // parameter to true
                    if (BooleanUtils.toBoolean(flag))
                    {
                        // that was original file name.
                        fileName = fileData.getValues().get(FileUploadEnum.file);
                    }
                    File withSuffixfile = FileUtils.getFile(descDir, fileName);
                    if (!withSuffixfile.exists())
                    {
                        // let move action to rename the template file.
                        FileUtils.moveFile(file, withSuffixfile);
                    }

                    // set readable state
                    withSuffixfile = FileUtils.getFile(descDir, fileName);
                    if (withSuffixfile.exists())
                    {
                        withSuffixfile.setReadOnly();
                    }
                    else
                    {
                        throw new RuntimeException(
                                        String.format(
                                            "MOVE FILE FAIL : the file content type is %s.\n source file path is \" %s \" is exists,but the target file \" %s \" isn't exists.  ",
                                            fileData.getContentType(), file.getPath(), withSuffixfile.getPath()));
                    }
                }
                catch (Exception e)
                {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                    // if
                    fileData.increase();
                    if (fileData.getOpsTime() < maxOperateTime)
                    {
                        errorList.add(fileData);
                    }
                }

                synchronized (cleanUpQueue)
                {
                    fileData = cleanUpQueue.poll();
                }

            }
            if (!errorList.isEmpty())
            {
                cleanUpQueue.addAll(errorList);
            }
        }

        String regEx = "([\\w]+)+(:/{2,})*([\\w.]+)+(:[\\d]*)?(\\/)?(\\/?+[\\w]*){1}";
        Pattern pattern = Pattern.compile(regEx);

        private String getDescFilePath(FileData fileData)
        {
            return fileData.getAttrs().get(FileUploadEnum.basefolder.toString());
        }

        private String getDescFilePath2(FileData fileData)
        {
            String resUrl = fileData.getImageUrl();
            Matcher mat = pattern.matcher(resUrl);
            // when URI remove it, left path was resource path.
            if (mat.find())
            {
                resUrl = StringUtils.remove(resUrl, mat.group());
            }
            return resUrl;
        }
    }

    static class FileUploadRequest implements Callable<List<FileData>>
    {
        ServletFileUploadProvider provider;
        HttpServletRequest request;
        HttpServletResponse response;

        FileUploadRequest(ServletFileUploadProvider provider, HttpServletRequest request, HttpServletResponse response)
        {
            this.request = request;
            this.response = response;
            this.provider = provider;
        }

        public List<FileData> call() throws Exception
        {
            String folder = request.getParameter("args");
            String parentFolder = request.getParameter("res");
            String referer = request.getHeader("Referer");
            System.out.println("======= > refer : " + referer);

            Enumeration<String> hearders = request.getHeaderNames();
            while (hearders.hasMoreElements())
            {
                String s = hearders.nextElement();
                System.out.println(s + "   " + request.getHeader(s));
            }

            Map<FileUploadEnum, String> dataMap = new HashMap<FileUploadEnum, String>();
            dataMap.put(FileUploadEnum.machineNo, parentFolder);
            dataMap.put(FileUploadEnum.uploadType, folder);
            dataMap.put(FileUploadEnum.root, config.rootPath);
            dataMap.put(FileUploadEnum.temp, config.tempPath);
            dataMap.put(FileUploadEnum.date, config.getDate());
            dataMap.put(FileUploadEnum.resServer, ConfigHelper.getStringProperty("app.fileupload.server"));

            try
            {
                List<FileItem> items = provider.getFileUpload().parseRequest(request);
                Map<String, FileData> fileDatamap = new LinkedHashMap<String, FileData>();
                for (FileItem fileItem : items)
                {

                    if (fileItem.isFormField())
                    {
                        if (!StringUtils.contains(fileItem.getFieldName(), "."))
                        {
                            continue;
                        }
                        String[] named = StringUtils.split(fileItem.getFieldName(), ".");
                        System.out.println(fileItem.getFieldName() + " !!!! + " + fileItem.getString());
                        if (named.length <= 0)
                        {
                            continue;
                        }

                        String objkey = named[0];
                        String fieldName = named[0];
                        if (named.length > 1)
                        {
                            fieldName = named[1];
                        }

                        FileData fileData = null;
                        if (fileDatamap.containsKey(objkey))
                        {
                            fileData = fileDatamap.get(objkey);
                        }
                        else
                        {
                            fileData = new FileData();
                            fileDatamap.put(objkey, fileData);
                            fileData.setValues(dataMap);
                            fileData.getValues().put(FileUploadEnum.fieldName, objkey);
                        }
                        // set field value
                        FileDataFieldEnum ff = FileDataFieldEnum.getFileDataField(fieldName);
                        if (!FileDataFieldEnum.UNKNOW.equals(ff))
                        {
                            ff.invoke(fileData.getFields(), fileItem.getString());
                        }
                    }
                }
                return new ArrayList<FileData>(fileDatamap.values());
            }
            catch (FileUploadException e1)
            {
                e1.printStackTrace();
            }
            return null;
        }

    }

    static class FileDirecotryStore
    {
        public FileDirecotryStore(File dir, String accessAllow)
        {
            super();
            this.dir = dir;
            this.accessAllow = accessAllow;
        }

        File dir;
        String accessAllow;
    }

    static class FileUploadConfig
    {

        private final static String PREFIX = "app.fileupload.";
        private final static String IMAGE_PATH = "image";
        int fileUploadPoolSize = 10;
        int initialDelay = 5;
        int period = 2;
        int uploadTimeout = 1000;
        String rootPath;
        String tempPath;
        Map<String, FileDirecotryStore> resoureMap = new HashMap<String, FileDirecotryStore>();
        Map<String, String> allowTypeMap = new HashMap<String, String>();
        File rootDirectory;
        File tempDirectory;
        int maxCountHandleImageMoveProcess = 50;

        static String MONTHDATE = "1223";
        static long dateStart = 0;
        static long dateEnd = 0;

        FileUploadConfig()
        {
            init();
        }

        private void init()
        {
            fileUploadPoolSize = ConfigHelper.getScopeNumberProperty(PREFIX, "poolsize", fileUploadPoolSize);
            uploadTimeout = ConfigHelper.getScopeNumberProperty(PREFIX, "timeout", uploadTimeout);
            initialDelay = ConfigHelper.getScopeNumberProperty(PREFIX, "cleanup.initialDelay", initialDelay);
            period = ConfigHelper.getScopeNumberProperty(PREFIX, "cleanup.period", period);
            maxCountHandleImageMoveProcess =
                            ConfigHelper.getScopeNumberProperty(PREFIX, "cleanup.maxCount",
                                maxCountHandleImageMoveProcess);

            rootPath = ConfigHelper.getScopeStringProperty(PREFIX, FileUploadEnum.root.toString());
            tempPath = ConfigHelper.getScopeStringProperty(PREFIX, FileUploadEnum.temp.toString());
            initFile();
        }

        private void initFile()
        {
            rootDirectory = readFileWithProperties(FileUploadEnum.root.toString());
            tempDirectory = readFileWithProperties(FileUploadEnum.temp.toString());
            String subFolder = ConfigHelper.getScopeStringProperty(PREFIX, FileUploadEnum.subfolder.toString());

            if (subFolder.length() > 0)
            {
                for (String subf : StringUtils.split(subFolder, ","))
                {
                    File file = readFileWithProperties(rootDirectory, subf);
                    if (null == file)
                    {
                        file = new File(rootDirectory, subf);
                    }
                    String accessAllowed =
                                    ConfigHelper.getScopeStringProperty(PREFIX + subf,
                                        FileUploadEnum.allowtype.toString());
                    resoureMap.put(subf, new FileDirecotryStore(file, accessAllowed));
                    if (StringUtils.contains(accessAllowed, ","))
                    {
                        for (String mine : StringUtils.split(accessAllowed, ","))
                        {
                            allowTypeMap.put(mine, subf);
                        }
                    }
                    else
                    {
                        allowTypeMap.put(accessAllowed, subf);
                    }
                }
            }

        }

        public String getResPath(String contentType)
        {
            if (allowTypeMap.containsKey(contentType))
            {
                return allowTypeMap.get(contentType);
            }
            String mappingAll = null;
            for (Map.Entry<String, String> entry : allowTypeMap.entrySet())
            {
                String key = entry.getValue();
                if (StringUtils.contains(contentType, "/"))
                {
                    String[] strs = StringUtils.split(contentType, "/");
                    for (String sub : strs)
                    {
                        if (StringUtils.contains(key, sub))
                        {
                            return allowTypeMap.get(key);
                        }
                    }
                }
                if (StringUtils.contains(key, "all"))
                {
                    mappingAll = key;
                }

            }
            if (null != mappingAll)
                return allowTypeMap.get(mappingAll);
            return "other";
        }

        private FileDirecotryStore getResource(String contentType)
        {
            return resoureMap.get(getResPath(contentType));
        }

        public boolean isImagePate(String contentType)
        {
            return IMAGE_PATH.equalsIgnoreCase(getResPath(contentType));
        }

        public File getResourceLocaltionPath(String contentType)
        {
            FileDirecotryStore store = getResource(contentType);
            if (null != store)
            {
                return store.dir;
            }
            return null;
        }

        private File readFileWithProperties(String key)
        {
            String path = ConfigHelper.getScopeStringProperty(PREFIX, key);
            if (StringUtils.isBlank(path))
            {
                return null;
            }
            return new File(path);
        }

        private File readFileWithProperties(File parent, String key)
        {
            String path = ConfigHelper.getScopeStringProperty(PREFIX, key);
            if (StringUtils.isBlank(path))
            {
                path = key;
            }
            return new File(parent, path);
        }

        private String getDate()
        {
            long time = System.currentTimeMillis();
            if (time > dateEnd)
            {
                Date d = new Date();
                Calendar cal = Calendar.getInstance();
                int year = cal.get(Calendar.YEAR);
                int month = cal.get(Calendar.MONTH) + 1;
                int day = cal.get(Calendar.DAY_OF_MONTH);
                Calendar cal2 = Calendar.getInstance();
                cal2.set(year + 1900, month, day);
                dateStart = cal2.getTime().getTime();
                dateEnd = dateStart + 86400000 - 500;
                String mm = String.valueOf(month);
                String dd = String.valueOf(day);

                MONTHDATE =
                                StringUtils.repeat("0", 2 - mm.length()) + mm
                                                + StringUtils.repeat("0", 2 - dd.length()) + dd;
            }
            return MONTHDATE;
        }
    }

    private void putAttr(FileData fileData, String subFolder, FileUploadEnum fileUploadEnum)
    {
        String value = ConfigHelper.getUpwardProperty(config.PREFIX, subFolder, fileUploadEnum);
        if (null != value)
        {
            fileData.addAttr(fileUploadEnum.toString(), value);
        }
    }

    private void putValue(FileData fileData, String subFolder, FileUploadEnum fileUploadEnum)
    {
        String value = ConfigHelper.getUpwardProperty(config.PREFIX, subFolder, fileUploadEnum);
        if (null != value)
        {
            fileData.addValue(fileUploadEnum, value);
        }
    }

    private String getFilePostfix(String name)
    {
        if (StringUtils.contains(name, '?'))
        {
            name = StringUtils.substringBefore(name, "?");
        }
        return StringUtils.substringAfterLast(name, ".");
    }

    private static String moveFilePath(String folder, String parentFolder)
    {

        return parentFolder + "/" + folder + "/" + config.getDate();
    }

}
